//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift.org project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift.org project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import JExtractSwiftLib
import Testing

@Suite
struct JNIClassTests {
  let source = """
      public class MyClass {
        let x: Int64
        let y: Int64

        public static func method() {}

        public init(x: Int64, y: Int64) {
          self.x = y
          self.y = y
        }

        public init() {
          self.x = 0
          self.y = 0
        }

        public func doSomething(x: Int64) {}

        public func copy() -> MyClass {}
        public func isEqual(to other: MyClass) -> Bool {}
      }
    """

  @Test
  func generatesJavaClass() throws {
    try assertOutput(
      input: source,
      .jni, .java,
      expectedChunks: [
        """
        // Generated by jextract-swift
        // Swift module: SwiftModule

        package com.example.swift;

        import org.swift.swiftkit.core.*;
        import org.swift.swiftkit.core.util.*;
        import java.util.*;
        import java.util.concurrent.atomic.AtomicBoolean;
        import org.swift.swiftkit.core.annotations.*;
        """,
        """
        public final class MyClass implements JNISwiftInstance {
          static final String LIB_NAME = "SwiftModule";

          @SuppressWarnings("unused")
          private static final boolean INITIALIZED_LIBS = initializeLibs();
          static boolean initializeLibs() {
            System.loadLibrary(LIB_NAME);
            return true;
          }
        """,
        """
        private final long selfPointer;
        """,
        """
        public long $memoryAddress() {
          return this.selfPointer;
        }
        """,
        """
        private MyClass(long selfPointer, SwiftArena swiftArena) {
          SwiftObjects.requireNonZero(selfPointer, "selfPointer");
          this.selfPointer = selfPointer;
        
          // Only register once we have fully initialized the object since this will need the object pointer.
          swiftArena.register(this);
        }
        """,
        """
        public static MyClass wrapMemoryAddressUnsafe(long selfPointer, SwiftArena swiftArena) {
          return new MyClass(selfPointer, swiftArena);
        }
        """
      ])
    try assertOutput(
      input: source,
      .jni, .java,
      expectedChunks: [
        """
        private static native void $destroy(long selfPointer);
        """
      ])
    try assertOutput(
      input: source,
      .jni, .java,
      expectedChunks: [
        """
        @Override
        public Runnable $createDestroyFunction() {
          long self$ = this.$memoryAddress();
          if (CallTraces.TRACE_DOWNCALLS) {
            CallTraces.traceDowncall("MyClass.$createDestroyFunction",
                "this", this,
                "self", self$);
          }
          return new Runnable() {
            @Override
            public void run() {
              if (CallTraces.TRACE_DOWNCALLS) {
                CallTraces.traceDowncall("MyClass.$destroy", "self", self$);
              }
              MyClass.$destroy(self$);
            }
          };
        """
      ])
  }

  @Test
  func staticMethod_javaBindings() throws {
    try assertOutput(
      input: source,
      .jni,
      .java,
      expectedChunks: [
        """
        /**
         * Downcall to Swift:
         * {@snippet lang=swift :
         * public static func method()
         * }
         */
        public static void method() {
          MyClass.$method();
        }
        """,
        """
        private static native void $method();
        """,
      ]
    )
  }

  @Test
  func staticMethod_swiftThunks() throws {
    try assertOutput(
      input: source,
      .jni,
      .swift,
      detectChunkByInitialLines: 1,
      expectedChunks: [
        """
        @_cdecl("Java_com_example_swift_MyClass__00024method__")
        func Java_com_example_swift_MyClass__00024method__(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass) {
          MyClass.method()
        }
        """
      ]
    )
  }

  @Test
  func initializer_javaBindings() throws {
    try assertOutput(
      input: source,
      .jni,
      .java,
      expectedChunks: [
        """
        /**
         * Downcall to Swift:
         * {@snippet lang=swift :
         * public init(x: Int64, y: Int64)
         * }
         */
        public static MyClass init(long x, long y, SwiftArena swiftArena$) {
          return MyClass.wrapMemoryAddressUnsafe(MyClass.$init(x, y), swiftArena$);
        }
        """,
        """
        /**
         * Downcall to Swift:
         * {@snippet lang=swift :
         * public init()
         * }
         */
        public static MyClass init(SwiftArena swiftArena$) {
          return MyClass.wrapMemoryAddressUnsafe(MyClass.$init(), swiftArena$);
        }
        """,
        """
        private static native long $init(long x, long y);
        """,
        """
        private static native long $init();
        """,
      ]
    )
  }

  @Test
  func initializer_swiftThunks() throws {
    try assertOutput(
      input: source,
      .jni,
      .swift,
      detectChunkByInitialLines: 1,
      expectedChunks: [
        """
        @_cdecl("Java_com_example_swift_MyClass__00024init__JJ")
        func Java_com_example_swift_MyClass__00024init__JJ(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass, x: jlong, y: jlong) -> jlong {
          let result$ = UnsafeMutablePointer<MyClass>.allocate(capacity: 1)
          result$.initialize(to: MyClass.init(x: Int64(fromJNI: x, in: environment), y: Int64(fromJNI: y, in: environment)))
          let resultBits$ = Int64(Int(bitPattern: result$))
          return resultBits$.getJNIValue(in: environment)
        }
        """,
        """
        @_cdecl("Java_com_example_swift_MyClass__00024init__")
        func Java_com_example_swift_MyClass__00024init__(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass) -> jlong {
          let result$ = UnsafeMutablePointer<MyClass>.allocate(capacity: 1)
          result$.initialize(to: MyClass.init())
          let resultBits$ = Int64(Int(bitPattern: result$))
          return resultBits$.getJNIValue(in: environment)
        }
        """,
      ]
    )
  }

  @Test
  func destroyFunction_swiftThunks() throws {
    try assertOutput(
      input: source,
      .jni,
      .swift,
      detectChunkByInitialLines: 1,
      expectedChunks: [
        """
        @_cdecl("Java_com_example_swift_MyClass__00024destroy__J")
        func Java_com_example_swift_MyClass__00024destroy__J(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass, selfPointer: jlong) {
          guard let env$ = environment else {
            fatalError("Missing JNIEnv in downcall to \\(#function)")
          }
          assert(selfPointer != 0, "selfPointer memory address was null")
          let selfBits$ = Int(Int64(fromJNI: selfPointer, in: env$))
          guard let self$ = UnsafeMutablePointer<MyClass>(bitPattern: selfBits$) else {
            fatalError("self memory address was null in call to \\(#function)!")
          }
          self$.deinitialize(count: 1)
          self$.deallocate()
        }
        """
      ]
    )
  }

  @Test
  func memberMethod_javaBindings() throws {
    try assertOutput(
      input: source,
      .jni,
      .java,
      expectedChunks: [
        """
        /**
         * Downcall to Swift:
         * {@snippet lang=swift :
         * public func doSomething(x: Int64)
         * }
         */
        public void doSomething(long x) {
          MyClass.$doSomething(x, this.$memoryAddress());
        }
        """,
        """
        private static native void $doSomething(long x, long self);
        """,
      ]
    )
  }

  @Test
  func memberMethod_swiftThunks() throws {
    try assertOutput(
      input: source,
      .jni,
      .swift,
      detectChunkByInitialLines: 1,
      expectedChunks: [
        """
        @_cdecl("Java_com_example_swift_MyClass__00024doSomething__JJ")
        func Java_com_example_swift_MyClass__00024doSomething__JJ(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass, x: jlong, self: jlong) {
          assert(self != 0, "self memory address was null")
          let selfBits$ = Int(Int64(fromJNI: self, in: environment))
          let self$ = UnsafeMutablePointer<MyClass>(bitPattern: selfBits$)
          guard let self$ else {
            fatalError("self memory address was null in call to \\(#function)!")
          }
          self$.pointee.doSomething(x: Int64(fromJNI: x, in: environment))
        }
        """
      ]
    )
  }

  @Test
  func methodReturningClass_javaBindings() throws {
    try assertOutput(
      input: source,
      .jni,
      .java,
      expectedChunks: [
        """
        /**
         * Downcall to Swift:
         * {@snippet lang=swift :
         * public func copy() -> MyClass
         * }
         */
        public MyClass copy(SwiftArena swiftArena$) {
          return MyClass.wrapMemoryAddressUnsafe(MyClass.$copy(this.$memoryAddress()), swiftArena$);
        }
        """,
        """
        private static native long $copy(long self);
        """,
      ]
    )
  }

  @Test
  func methodReturningClass_swiftThunks() throws {
    try assertOutput(
      input: source,
      .jni,
      .swift,
      detectChunkByInitialLines: 1,
      expectedChunks: [
        """
        @_cdecl("Java_com_example_swift_MyClass__00024copy__J")
        func Java_com_example_swift_MyClass__00024copy__J(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass, self: jlong) -> jlong {
          assert(self != 0, "self memory address was null")
          let selfBits$ = Int(Int64(fromJNI: self, in: environment))
          let self$ = UnsafeMutablePointer<MyClass>(bitPattern: selfBits$)
          guard let self$ else {
            fatalError("self memory address was null in call to \\(#function)!")
          }
          let result$ = UnsafeMutablePointer<MyClass>.allocate(capacity: 1)
          result$.initialize(to: self$.pointee.copy())
          let resultBits$ = Int64(Int(bitPattern: result$))
          return resultBits$.getJNIValue(in: environment)
        }
        """
      ]
    )
  }

  @Test
  func classAsParameter_javaBindings() throws {
    try assertOutput(
      input: source,
      .jni,
      .java,
      expectedChunks: [
        """
        /**
         * Downcall to Swift:
         * {@snippet lang=swift :
         * public func isEqual(to other: MyClass) -> Bool
         * }
         */
        public boolean isEqual(MyClass other) {
          return MyClass.$isEqual(other.$memoryAddress(), this.$memoryAddress()); 
        }
        """,
        """
        private static native boolean $isEqual(long other, long self);
        """,
      ]
    )
  }

  @Test
  func classAsParameter_swiftThunks() throws {
    try assertOutput(
      input: source,
      .jni,
      .swift,
      detectChunkByInitialLines: 1,
      expectedChunks: [
        """
        @_cdecl("Java_com_example_swift_MyClass__00024isEqual__JJ")
        func Java_com_example_swift_MyClass__00024isEqual__JJ(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass, other: jlong, self: jlong) -> jboolean {
          assert(other != 0, "other memory address was null")
          let otherBits$ = Int(Int64(fromJNI: other, in: environment))
          let other$ = UnsafeMutablePointer<MyClass>(bitPattern: otherBits$) 
          guard let other$ else {
            fatalError("other memory address was null in call to \\(#function)!")
          }
          assert(self != 0, "self memory address was null")
          let selfBits$ = Int(Int64(fromJNI: self, in: environment))
          let self$ = UnsafeMutablePointer<MyClass>(bitPattern: selfBits$)
          guard let self$ else {
            fatalError("self memory address was null in call to \\(#function)!")
          }
          return self$.pointee.isEqual(to: other$.pointee).getJNIValue(in: environment)
        }
        """
      ]
    )
  }
}
